在前面幾個章節中,我們完成了對 React 環境的整合應用,接者我們來嘗試導入 Vue 環境使用。
把我們的 signal
/ computed
安全地接到 Vue 3 Composition API,讓模板可直接使用,同時維持我們自家 reactive graph 的行為(push 標髒標記 + pull 重算)、避免雙重依賴與時序打架。
ref
;不要把 Vue 的 reactivity 反向接回你的圖,避免排程繞圈。onUnmounted
清理 createEffect
與(如果有)computed.dispose()
。peek()
(不追蹤、必要時 lazy 重算),在 our effect 內用 get()
(建立依賴)。useComputedRef
的 callback 必須讀 signal.get()
才會被追蹤;若只是純 Vue 計算,請用 Vue 的 computed
。watch*
只看 Vue ref
(透過 useSignalRef
),不直接 .get()
。computed
在 callback 裡讀 signal.get()
建依賴。import { shallowRef, onUnmounted, type Ref } from "vue";
import { createEffect, onCleanup } from "../core/effect.js";
import { computed as coreComputed } from "../core/computed.js";
type Readable<T> = { get(): T; peek(): T };
// 將 signal/computed 映射為 Vue ref(tear-free;由我們的 effect 推動)
export function useSignalRef<T>(src: Readable<T>): Ref<T> {
const r = shallowRef<T>(src.peek()) as Ref<T>; // 初始快照(不追蹤)
const stop = createEffect(() => {
// 在追蹤上下文中讀取,值變動時同步寫入 Vue ref
r.value = src.get();
onCleanup(() => {
// optional:保留擴充(例如取消計時器),目前無需特別清理
});
});
onUnmounted(() => stop()); // 元件卸載即解除訂閱
return r;
}
// 在元件生命週期內建立你的 computed,並以 Vue ref 暴露
export function useComputedRef<T>(
fn: () => T,
equals: (a: T, b: T) => boolean = Object.is
): Ref<T> {
// 注意:fn 內要讀 signal.get() 才會建立依賴
const memo = coreComputed(fn, equals);
const r = useSignalRef<T>({ get: () => memo.get(), peek: () => memo.peek() });
onUnmounted(() => memo.dispose?.());
return r;
}
為什麼用 shallowRef
?
我們已在 core 內做等值判斷與快取,Vue 端只需感知「值是否改變」。深層追蹤交給 core 的等值策略(equals
)處理。
peek()
拿快照;在 effect 中用 get()
建依賴。onUnmounted → stop()
,確保不殘留訂閱。signal
+ 衍生值<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useSignalRef, useComputedRef } from "./vue-adapter";
const countSig = signal(0);
const count = useSignalRef(countSig); // Vue ref
const doubled = useComputedRef(() => countSig.get() * 2); // 讀 .get() 建依賴
const inc = () => countSig.set(v => v + 1);
</script>
<template>
<p>{{ count }} / {{ doubled }}</p>
<button @click="inc">+1</button>
</template>
<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useComputedRef } from "./vue-adapter";
const userSig = signal({ id: 1, name: "Ada", age: 37 });
// 只暴露 name;即使物件其它欄位變了,只要 name 相等就不觸發模板更新
const nameRef = useComputedRef(
() => userSig.get().name,
(a, b) => a === b
);
</script>
<template>
<h2>{{ nameRef }}</h2>
</template>
useComputedRef
,卸載時自動 dispose()
。computed
,務必在確定不再使用時手動 dispose()
。watch
/ watchEffect
如何分工?signal
/computed
先用 useSignalRef
變成 Vue ref
,再用 watch
/ watchEffect
。
這樣 Vue 只觀察「值」,不會參與你的依賴圖,避免雙重追蹤。
const price = useSignalRef(priceSig);
watch(price, (nv, ov) => {
console.log("price changed:", ov, "→", nv);
});
不要在 watchEffect
內直接讀 signal.get()
,那會讓 Vue 的追蹤也加入我們的圖,可能造成不必要重跑與生命週期打結。
onMounted
/ watch
。watch
要看 useSignalRef
轉出的 Vue ref
,不要直接使用 .get()
。useComputedRef(() => ref.value * 2)
signal.get()
;你如果真的只要 Vue 計算,直接用 Vue 的 computed
。setup
直讀 signal.get()
useSignalRef
暴露為 ref
再給模板。ref
與 signal 雙向驅動useSignalRef(signalOrComputed)
useComputedRef(() => signal.get() + ...)
computed
useSignalRef
,再用 watch
/ watchEffect
.get()
;在 useComputedRef
讀 ref.value
如果是從前面一路追過來的朋友,應該很熟悉這套流程了,對於我們開發的 signal 來說,框架只剩下與 UI 綁定渲染的功能,所以製作 Adapter 來說,就是多做多熟練,相較於 React 的特殊性,Vue 這樣採用模板渲染的就會更加貼近我們的心智模型,只要掌握以下就能順順使用了:
Vue 端只把 值(ref) 展現出來;依賴追蹤與快取仍交給我們的 signal 系統。
這樣做能避免雙重依賴與時序打架,保留 push 標髒標記 + pull 重算 的核心優勢。
你現在已能把 signal 穩定地接進 Vue,下一篇我們把交互操作與進階場景補齊。